# -*- coding: utf-8 -*-

# @File   : xml
# @Author : Harold
# @Date   : 2018/2/26

from .common import *
from apps.storage.models.template import Template, TemplateFieldEnum
from apps.storage.models.data import DataMeta
from apps.storage.utils.serializers.data import *
from mongoengine import DoesNotExist
from lxml import etree
from django.conf import settings
from django.utils.translation import ugettext as _
from .json import JSONSerializer


class XMLHandler:

    def __init__(self, mode: DataMode, template: Template = None, exclude: tuple = None):
        self._mode = mode
        self._template = template
        self._path_str_field_meta_map = {}
        if self._mode == DataMode.WRITE or self._mode == DataMode.TEMPLATE:
            if template is None:
                raise ValueError(f'A template must be provided in {self._mode.description} mode.')
            self._load_template(template, exclude)
        self._root_node = etree.Element('root')

    def _load_template(self, template: Template, exclude: tuple):
        if self._mode == DataMode.WRITE and exclude:  # 导出模式时，先用exclude列表处理template
            exclude_fields(template, exclude)
        _ord = template.content.get('_ord', filter(lambda x: not x.startswith('_'), template.content.keys()))

        def _load_field(field_path_list, field_meta):

            path_str = '.'.join(field_path_list)  # path字符串
            self._path_str_field_meta_map[path_str] = field_meta  # 保存字段的meta信息
            t = TemplateFieldEnum(field_meta['t'])
            misc = field_meta.get('misc', {})

            if t.is_array:
                element_t = TemplateFieldEnum(field_meta.get('misc', {})['t'])
                _load_field(field_path_list + [str(element_t.value)], field_meta.get('misc', {}))
                return
            if t.is_table:
                sub_ord = misc['_head']
            elif t.is_container or t.is_generator:
                sub_ord = misc['_ord'] if '_ord' in misc else misc['_opt']
            else:
                return
            for sub_field_name in sub_ord:
                _load_field(field_path_list + [sub_field_name], field_meta=misc[sub_field_name])

        for field_name in _ord:
            _load_field([field_name], field_meta=template.content[field_name])

    def write_data(self, data_meta: DataMeta, with_none=True, short_data_id=None):
        if self._mode != DataMode.WRITE:
            raise ValueError(f"Cannot write XML in {self._mode.description} mode.")
        if not isinstance(data_meta, DataMeta):
            raise ValueError("data_meta should be an instance of DataMeta")
        if data_meta.tid != self._template.id:
            raise ValueError(f'Data\'s tid should be "{self._template.id}", got "{data_meta.tid}" instead')

        def _to_xml_value(field_meta, mongo_value):
            t = TemplateFieldEnum(field_meta['t'])
            if t.is_range:
                range_type = field_meta.get('misc', {})['type']
                if range_type == 0:
                    return '%s, %s' % (str(mongo_value['lb']), str(mongo_value['ub']))
                return '%s, %s' % (str(mongo_value['val']), str(mongo_value['err']))
            elif t.is_file or t.is_image:
                return [settings.SITE_ADDR + x for x in mongo_value]
            else:
                return str(mongo_value)

        def _add_field(field_path, field_data, in_array=False):
            if field_data is None or (isinstance(field_data, list) and len(field_data) == 0):
                if with_none and not in_array:
                    return etree.Element('field', name=field_path[-1])
                else:
                    return None
            field_meta = self._path_str_field_meta_map['.'.join(field_path)]
            if in_array:
                node = etree.Element('element')
            else:
                node = etree.Element('field', name=field_path[-1])
            t = TemplateFieldEnum(field_meta['t'])

            if t.is_table:
                headers = field_meta.get('misc', {})['_head']
                rows = DataFieldTableRow.objects(_owner_id=field_data)
                for row in rows:
                    row_node = etree.SubElement(node, 'row')
                    for header in headers:
                        sub_node = _add_field(field_path + [header], getattr(row, header, None))
                        if sub_node is not None:
                            row_node.append(sub_node)
            elif t.is_generator:
                sel = field_data['sel']
                node.append(_add_field(field_path + [sel], field_data['val']))
            elif t.is_container:
                for sub_field in field_meta.get('misc', {})['_ord']:
                    try:
                        container_content = DataFieldContainer.objects.get(pk=field_data)
                    except DoesNotExist:
                        container_content = {}
                    if sub_field in container_content:
                        sub_node = _add_field(field_path + [sub_field], getattr(container_content, sub_field, None))
                        if sub_node is not None:
                            node.append(sub_node)
            elif t.is_array:
                for value in field_data:
                    sub_node = _add_field(field_path + [str(field_meta.get('misc', {})['t'])], value, in_array=True)
                    if sub_node is not None:
                        node.append(sub_node)
            elif t.is_file or t.is_image:
                hyperlinks = _to_xml_value(field_meta=field_meta, mongo_value=field_data)
                link_node_name = 'image' if t.is_image else 'file'
                for link in hyperlinks:
                    link_node = etree.SubElement(node, link_node_name)
                    link_node.text = link
            else:
                unit = field_meta.get('misc', {}).get('unit')
                if unit is not None:
                    node.attrib['unit'] = unit
                node.text = _to_xml_value(field_meta=field_meta, mongo_value=field_data)
            return node

        data_node = etree.SubElement(self._root_node, 'data')
        meta_node = etree.SubElement(data_node, 'meta')
        content_node = etree.SubElement(data_node, 'content')

        for field in DataMetaFieldEnum.writer_iterator(with_id=True):
            value = field.get_value(data_meta)
            if (value is None or getattr(value, '__len__', int)() == 0) and not with_none:
                continue
            node = etree.Element('field', name=field.description)
            if value is not None:
                node.text = str(value)
            meta_node.append(node)

        try:
            content = DataContent.objects.get(pk=data_meta.dc_id)
        except DoesNotExist:
            content = {}  # TODO 找不到

        for field in self._template.content['_ord']:
            node = _add_field([field], getattr(content, field, None))
            if node is not None:
                content_node.append(node)

    def read_data(self, xml_path_or_fp, file_dir=None, author=None):
        if self._mode != DataMode.READ:
            raise ValueError("Cannot read XML in %s mode." % (self._mode.description,))

        parser = Parser()
        current_path_str = ""

        def _raise_parsing_exception(error: ParsingErrorEnum, o1=None, o2=None, o3=None, line=None):
            """
            报错
            :param error: ParsingErrorEnum枚举
            :param o1: 参数1
            :param o2: 参数2
            :param o3: 参数3
            :return:
            """
            kwargs = {}
            if o1:
                kwargs['o1'] = o1
            if o2:
                kwargs['o2'] = o1
            if o3:
                kwargs['o3'] = o3
            if line:
                suffix = _(" (line %(line_number)s)") % {'line_number': line}
            else:
                suffix = ""
            if isinstance(error, ParsingErrorEnum):
                e = ParsingError(error, **kwargs)
            else:
                e = error
            e.message += suffix
            raise e

        def _construct_field(field_meta, value):
            """
            根据值构造json中的字段
            :param field_meta: 字段元信息
            :param value: 字段的值
            :return: json中的字段
            """
            t = TemplateFieldEnum(field_meta['t'])
            v = {}
            if t.is_table:
                v = value
            elif t.is_array:
                v = value
            elif t.is_generator:
                v['sel'] = list(value.keys())[0]
                v['val'] = list(value.values())[0]
            elif t.is_container:
                v = value
            else:
                try:
                    v = parser.parse(current_path_str, field_meta, value)
                except ParsingError as e:
                    _raise_parsing_exception(e)
            return v

        def _read_field(field_path_list, parent_node: etree.Element, in_array=False):
            """
            读取某个字段的值
            :param field_path_list: 字段path列表
            :param node: 元素的节点
            :return: 字段的json值
            """
            nonlocal current_path_str
            field_meta = self._path_str_field_meta_map['.'.join(field_path_list)]
            t = TemplateFieldEnum(field_meta['t'])
            required = field_meta.get('misc', {}).get('r')
            if in_array:
                nodes = parent_node.findall('element')
                current_field_name = field_path_list[-2]
                current_path_str = '.'.join(field_path_list[:-2])
            else:
                node = parent_node.find(f"field[@name='{field_path_list[-1]}']")
                nodes = [] if node is None else [node]
                current_path_str = '.'.join(field_path_list[:-1])
                current_field_name = field_path_list[-1]
            if required and len(nodes) == 0:
                _raise_parsing_exception(ParsingErrorEnum.FIELD_MISSING, o1=current_path_str,
                                         line=parent_node.sourceline)

            all_data = []

            for node in nodes:
                if t.is_container:
                    raw_data = {}
                    _ord = field_meta.get('misc', {})['_ord']
                    for field_name in _ord:
                        raw_data[field_name] = _read_field(field_path_list + [field_name], node)
                elif t.is_array:
                    element_t = field_meta.get('misc', {})['t']
                    raw_data = _read_field(field_path_list + [str(element_t)], parent_node=node, in_array=True)
                elif t.is_table:
                    rows = node.findall('row')
                    if len(rows) == 0:
                        if required:
                            _raise_parsing_exception(ParsingErrorEnum.NODE_MISSING, o1='row',
                                                     o2=f'field({current_path_str})')
                    raw_data = []
                    for row in rows:
                        raw_data.append({})
                        for header in field_meta.get('misc', {})["_head"]:
                            raw_data[-1][header] = _read_field(field_path_list + [header], parent_node=row)
                elif t.is_generator:
                    selected_node = node.find('field[1]')
                    if selected_node is None:
                        _raise_parsing_exception(ParsingErrorEnum.VALUE_MISSING, o1=current_field_name,
                                                 line=node.sourceline)
                    selected_name = selected_node.attrib.get('name')
                    if selected_name is None:
                        _raise_parsing_exception(ParsingErrorEnum.XML_ATTRIBUTE_MISSING, o1='name', o2='field',
                                                 line=node.sourceline)
                    if selected_name not in field_meta.get('misc', {})['_opt']:
                        _raise_parsing_exception(ParsingErrorEnum.UNKNOWN_FIELD, o1=selected_node.tag,
                                                 line=node.sourceline)
                    raw_data = {selected_name: _read_field(field_path_list + [selected_name], node)}
                elif t.is_file or t.is_image:
                    files = node.findall('image' if t.is_image else 'file')
                    raw_data = ','.join([x.text for x in files])
                    if len(raw_data) == 0:
                        if required:
                            _raise_parsing_exception(ParsingErrorEnum.VALUE_MISSING, o1=current_path_str)
                else:
                    text = node.text
                    if text is None or text.strip() == '':
                        if required:
                            _raise_parsing_exception(ParsingErrorEnum.VALUE_MISSING, o1=current_path_str,
                                                     line=node.sourceline)
                        raw_data = None
                    else:
                        # if t.is_choice:
                        #     choices = choices_to_list(field_meta.get('misc', {}))
                        #     if text not in choices:
                        #         _raise_parsing_exception(ParsingErrorEnum.INVALID_CHOICE_VALUE, o1=text,
                        #                                  o2=', '.join(choices), line=node.sourceline)
                        raw_data = text
                all_data.append(_construct_field(field_meta, raw_data))
            if in_array:
                return all_data
            elif len(all_data) == 1:
                return all_data[0]
            else:
                return None

        def _read_data(data_node):
            _ord = self._template.content.get('_ord',
                                              filter(lambda x: not x.startswith('_'),
                                                     self._template.content.keys()))
            data = {}
            for field_name in _ord:
                data[field_name] = _read_field([field_name], data_node)
            return data

        def _read_meta(meta_node):
            temp_meta = {}
            for meta_field in DataMetaFieldEnum.reader_iterator():
                field_node = meta_node.find(f"field[@name='{meta_field.description}']")
                if field_node is None:
                    _raise_parsing_exception(ParsingErrorEnum.FIELD_MISSING, o1=meta_field.description)
                if field_node.text is None or field_node.text.strip() == '':
                    _raise_parsing_exception(ParsingErrorEnum.VALUE_MISSING, o1=meta_field.description)
                temp_meta[meta_field.field_raw_name] = field_node.text
            source = dict(source=temp_meta.get('source'))
            temp_meta['source'] = {}
            source['project'] = temp_meta.get('project')
            source['others'] = temp_meta.get('others')
            source['reference'] = temp_meta.get('reference')
            for k, v in source.items():
                if v is not None:
                    temp_meta['source'][k] = v
            temp_meta.pop('project', None)
            temp_meta.pop('reference', None)
            temp_meta.pop('others', None)
            temp_meta.pop(None, None)
            return temp_meta

        try:
            root = etree.parse(xml_path_or_fp).getroot()
        except etree.XMLSyntaxError as e:
            _raise_parsing_exception(ParsingErrorEnum.XMLSyntaxError, o1=str(e))
        tid = root.attrib.get('tid')
        if tid is None:
            _raise_parsing_exception(ParsingErrorEnum.BAD_TEMPLATE)
        else:
            try:
                template = Template.objects.get(id=tid)
                self._load_template(template, exclude=())
            except Template.DoesNotExist:
                _raise_parsing_exception(ParsingErrorEnum.TEMPLATE_DOES_NOT_EXIST, o1=tid)
        data_root_nodes = root.xpath('data')
        meta_list = []
        content_list = []
        for data_root_node in data_root_nodes:
            meta_node = data_root_node.find('meta')
            if meta_node is None:
                _raise_parsing_exception(ParsingErrorEnum.NODE_MISSING, o1='meta', o2='data',
                                         line=data_root_node.sourceline)
            data_node = data_root_node.find('content')
            if data_node is None:
                _raise_parsing_exception(ParsingErrorEnum.NODE_MISSING, o1='content', o2='data',
                                         line=data_root_node.sourceline)
            meta_list.append(_read_meta(meta_node))
            content_list.append(_read_data(data_node))
        dm_list = []
        js = JSONSerializer(template)
        for meta, content in zip(meta_list, content_list):
            dm = js.get_deserialized_obj(dict(meta=meta, content=content), wd=file_dir)
            dm_list.append(dm)
        return dm_list

    def generate_template(self, number_of_data=2, elements_per_array=2, rows_per_table=2):
        """
        生成导入模板
        :param number_of_data:
        :param elements_per_array:
        :param rows_per_table:
        :return:
        """

        def _add_field(field_path, in_array=False):
            field_meta = self._path_str_field_meta_map['.'.join(field_path)]
            if in_array:
                node = etree.Element('element')
            else:
                node = etree.Element('field', name=field_path[-1])
            t = TemplateFieldEnum(field_meta['t'])
            if t.is_table:
                headers = field_meta.get('misc', {})['_head']
                for index in range(1, rows_per_table + 1):
                    row_node = etree.SubElement(node, 'row')
                    row_node.append(etree.Comment(
                        _("***Fill row %(row_number)s below and remove this line***") % {'row_number': index}))
                    for header in headers:
                        row_node.append(_add_field(field_path + [header]))
            elif t.is_generator:
                node.append(etree.Comment(_('***Keep only ONE of the following fields and remove this line***')))
                for option in field_meta.get('misc', {})['_opt']:
                    node.append(_add_field(field_path + [option]))
            elif t.is_container:
                for sub_field in field_meta.get('misc', {})['_ord']:
                    sub_node = _add_field(field_path + [sub_field])
                    node.append(sub_node)
            elif t.is_array:
                for index in range(1, elements_per_array + 1):
                    node.append(etree.Comment(_("***Fill element %(element_number)s below and remove this line***") % {
                        'element_number': index}))
                    node.append(_add_field(field_path + [str(field_meta.get('misc', {})['t'])], in_array=True))
            elif t.is_file or t.is_image:
                link_node_name = 'image' if t.is_image else 'file'
                for index in range(1, elements_per_array + 1):
                    link_node = etree.SubElement(node, link_node_name)
                    link_node.text = _("***Fill %(type_name)s %(file_number)s's path here***") % {
                        'type_name': link_node_name,
                        'file_number': str(index)}
            else:
                context = {'field_name': field_path[-1], 'field_type': t.description}
                if t.is_choice:
                    context['choices'] = ', '.join(choices_to_list(field_meta.get('misc', {})))
                    node.text = _(
                        '***Fill %(field_name)s (%(field_type)s) here. Choices are %(choices)s ***') % context
                else:
                    if in_array:
                        node.text = _('***Fill array element (%(field_type)s) here***') % context
                    else:
                        node.text = _('***Fill %(field_name)s (%(field_type)s) here***') % context
            return node

        for index in range(1, number_of_data + 1):
            data_node = etree.SubElement(self._root_node, 'data')
            meta_node = etree.SubElement(data_node, 'meta')
            content_node = etree.SubElement(data_node, 'content')
            for field in DataMetaFieldEnum.writer_iterator(with_id=True):
                field_node = etree.Element('field', name=field.description)
                if field == DataMetaFieldEnum.data_id:
                    _("***Data %(data_number)s's ID***") % {'data_number': index}
                field_node.text = _('***Fill %(field_name)s here***') % {'field_name': field.description}
                meta_node.append(field_node)

            for field in self._template.content['_ord']:
                node = _add_field([field])
                content_node.append(node)

    def save(self, fp_or_path):
        if self._mode == DataMode.READ:
            raise ValueError("Nothing to save in read mode.")
        content_to_save = etree.tostring(self._root_node, encoding='utf-8', xml_declaration=True,
                                         pretty_print=True)
        if hasattr(fp_or_path, 'write'):
            fp_or_path.write(content_to_save)
        else:
            with open(fp_or_path, 'wb') as fp:
                fp.write(content_to_save)
